#
# Copyright 2009 Red Hat, Inc. and/or its affiliates.
#
# Licensed to you under the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.  See the files README and
# LICENSE_GPL_v2 which accompany this distribution.
#


import os.path
import uuid

from config import config
import storage_exception as se
import volume
import image
import sd
import misc
import task
from threadLocal import vars
from sdf import StorageDomainFactory as SDF

class BlockVolume(volume.Volume):
    """ Actually represents a single volume (i.e. part of virtual disk).
    """
    def __init__(self, sdUUID, spUUID, imgUUID, volUUID):
        self.vg = SDF.produce(sdUUID=sdUUID).vg
        self.metaoff = None
        volume.Volume.__init__(self, sdUUID, spUUID, imgUUID, volUUID)

    def validate(self):
        if not self.vg.accessLV(self.volUUID):
            raise se.VolumeDoesNotExist(self.volUUID)
        volume.Volume.validate(self)


    @classmethod
    def refreshVolume(cls, sdobj, imgUUID, volUUID):
        sdobj.vg.refreshLV(volUUID)


    @classmethod
    def getVSize(cls, sdobj, imgUUID, volUUID, bs=512):
        return int(int(sdobj.vg.getLVInfo(volUUID))/bs)

    getVTrueSize = getVSize


    @classmethod
    def halfbakedVolumeRollback(cls, taskObj, sdUUID, volUUID, volPath):
        cls.log.info("halfbakedVolumeRollback: sdUUID=%s volUUID=%s volPath=%s" % (sdUUID, volUUID, volPath))
        sdvg = SDF.produce(sdUUID=sdUUID).vg
        if sdvg.accessLV(volUUID):
            sdvg.removeLV(volUUID)

        if os.path.lexists(volPath):
            os.unlink(volPath)


    @classmethod
    def createVolumeMetadataRollback(cls, taskObj, sdUUID, offs):
        cls.log.info("createVolumeMetadataRollback: sdUUID=%s offs=%s" % (sdUUID, offs))
        vg = SDF.produce(sdUUID=sdUUID).vg
        metaid = [vg, int(offs)]
        cls.__putMetadata({ "NONE": "#" * (sd.METASIZE-10) }, metaid)


    @classmethod
    def create(cls, isStopping, sdUUID, spUUID, imgUUID, size, volFormat, preallocate, diskType, volUUID, desc, srcImgUUID, srcVolUUID):
        """
       Create a new volume with given size or snapshot
            'size' - in sectors
            'volFormat' - volume format COW / RAW
            'preallocate' - Prealocate / Sparse
            'diskType' - string that describes disk type System|Data|Shared|Swap|Temp
            'srcImgUUID' - source image UUID
            'srcVolUUID' - source volume UUID
        """
        if not volUUID:
            volUUID = uuid.generate()
        if volUUID == volume.BLANK_UUID:
            raise se.InvalidParameterException("volUUID", volUUID)

        mysd = SDF.produce(sdUUID=sdUUID)
        sdvg = mysd.vg
        if sdvg.accessLV(volUUID):
            raise se.VolumeAlreadyExists(volUUID)

        image_dir = image.Image().create(sdUUID, spUUID, imgUUID, diskType)
        vol_path = os.path.join(image_dir, volUUID)
        pvol = None
        res = None
        voltype = "LEAF"

        try:
            if srcVolUUID != volume.BLANK_UUID:
                # We have a parent
                if srcImgUUID == volume.BLANK_UUID:
                    srcImgUUID = imgUUID
                pvol = BlockVolume(sdUUID, spUUID, srcImgUUID, srcVolUUID)
                # Cannot create snapshot for ILLEGAL volume
                if not pvol.isLegal():
                    raise se.createIllegalVolumeSnapshotError(pvol.volUUID)

                if imgUUID != srcImgUUID:
                    pvol.share(image_dir, hard=False)
                    pvol = BlockVolume(sdUUID, spUUID, imgUUID, srcVolUUID)

                # override size param by parent's size
                size = pvol.getSize()
        except se.StorageException, e:
            misc.logException(e)
            raise
        except Exception, e:
            misc.logException(e)
            raise se.VolumeCannotGetParent("blockVolume can't get parent %s for volume %s: %s" % (srcVolUUID, volUUID, str(e)))

        try:
            cls.log.info("blockVolume: creating LV: volUUID %s" % (volUUID))
            if preallocate == volume.SPARSE_VOL:
                volsize = "%sM" % config.get("irs", "volume_utilization_chunk_mb")
            else:
                # should stay %d and size should be int(size)
                volsize = "%sM" % (size / 2 / 1024)
            vars.task.pushRecovery(task.Recovery("halfbaked volume rollback", "blockVolume", "BlockVolume", "halfbakedVolumeRollback",
                                                 [sdUUID, volUUID, vol_path]))
            sdvg.createLV(volUUID, volsize, activate=True)
            if os.path.exists(vol_path):
                os.unlink(vol_path)
            os.symlink(sdvg.lvPath(volUUID), vol_path)
        except se.StorageException, e:
            misc.logException(e)
            raise
        except Exception, e:
            misc.logException(e)
            raise se.VolumeCreationError("blockVolume create/link lv %s failed: %s" % (volUUID, str(e)))

        try:
            cls.log.info("blockVolume: create: volUUID %s srcImg %s srvVol %s" % (volUUID, srcImgUUID, srcVolUUID))
            if not pvol:
                cls.log.info("(blockVolume.create) Request to create %s volume %s with size = %s sectors", volume.type2name(volFormat), vol_path, size)
                # Create 'raw' volume via qemu-img actually redundant
                if volFormat == volume.COW_FORMAT:
                    volume.createVolume(None, None, vol_path, size, volFormat, preallocate)
            else:
                ## Create hardlink to template and its meta file
                cls.log.info("(blockVolume.create) Request to create snapshot %s/%s of volume %s/%s", imgUUID, volUUID, srcImgUUID, srcVolUUID)
                pvol.clone(image_dir, volUUID, volFormat, preallocate)
        except Exception, e:
            misc.logException(e)
            raise

        try:
            offs = mysd.mapMetaOffset(volUUID)
            vars.task.pushRecovery(task.Recovery("create block volume metadata rollback", "blockVolume", "BlockVolume", "createVolumeMetadataRollback",
                                                 [sdUUID, str(offs)]))
            cls.newMetadata([sdvg, offs], spUUID, sdUUID, imgUUID, srcVolUUID,
                            size, volume.type2name(volFormat),
                            volume.type2name(preallocate), voltype, diskType, desc)
            sdvg.tagLV(volUUID, "MD_%s" % offs)
            sdvg.tagLV(volUUID, "PU_%s" % srcVolUUID)
            sdvg.tagLV(volUUID, "IU_%s" % imgUUID)
            sdvg.deactivateLV(volUUID)
        except se.StorageException, e:
            misc.logException(e)
            raise
        except Exception, e:
            misc.logException(e)
            raise se.VolumeMetadataWriteError("tag target volume %s failed: %s" % (volUUID, str(e)))

        # By definition volume is now a leaf
        sdvg.setrwLV(volUUID, rw=True)
        # Remove all previous rollbacks for 'halfbaked' volume and add rollback for 'real' volume creation
        vars.task.replaceRecoveries(task.Recovery("create block volume rollback", "blockVolume", "BlockVolume", "createVolumeRollback",
                                             [spUUID, sdUUID, imgUUID, volUUID, image_dir]))
        # Mark volume as LEGAL
        SDF.produce(sdUUID).produceVolume(imgUUID, volUUID).setLegality(volume.LEGAL_VOL)

        return volUUID


    def delete(self, isStopping, postZero, force):
        """ Delete volume
            'postZero' - zeroing file before deletion
            'force' is required to remove shared and internal volumes
        """
        self.log.info("(BlockVolume.delete) Request to delete LV %s of image %s in VG %s ",
                      self.volUUID, self.imgUUID, self.sdUUID)

        vol_path = self.getVolumePath()
        size = self.getVolumeSize(bs=1)
        offs = self.getMetaOffset()

        if not force:
            self.validateDelete()

        # Mark volume as illegal before deleting
        self.setLegality(volume.ILLEGAL_VOL)

        if postZero:
            self.prepare(justme=True, rw=True, chainrw=force, setrw=True, force=True)
            # wipe out the whole volume
            idle = config.getfloat('irs', 'idle')
            try:
                misc.ddWatchCopy("/dev/zero", vol_path, isStopping, idle, int(size))
            except Exception, e:
                misc.logException(e)
                raise se.VolumesZeroingError(vol_path)
            self.teardown(justme=True)

        # try to cleanup as much as possible
        eFound = False
        try:
            # We need to blank parent record in our metadata
            # for parent to become leaf successfully.
            puuid = self.getParent()
            self.parent = None
            self.setMetaParam(volume.PUUID, volume.BLANK_UUID)
            if puuid and puuid != volume.BLANK_UUID:
                pvol = BlockVolume(self.sdUUID, self.spUUID, self.imgUUID, puuid)
                pvol.recheckIfLeaf()
        except Exception, e:
            eFound = True
            misc.logException(e)
            self.log.error("cannot finalize parent volume %s: %s" % (puuid, str(e)))

        try:
            self.removeMetadata([self.vg, offs])
        except Exception, e:
            eFound = True
            misc.logException(e)
            self.log.error("cannot remove volume's %s metadata: %s" % (self.volUUID, str(e)))

        try:
            if self.vg and self.volUUID:
                if self.vg.accessLV(self.volUUID):
                    self.vg.removeLV(self.volUUID)

            os.unlink(vol_path)
        except Exception, e:
            eFound = True
            misc.logException(e)
            self.log.error("cannot unlink %s: %s" % (vol_path, str(e)))

        if eFound:
            raise se.CannotDeleteVolume(self.volUUID)

        return True


    def extend(self, newSize):
        """Extend a logical volume
            'newSize' - new size in blocks
        """
        self.log.info("(BlockVolume.extend) Request to extend LV %s of image %s in VG %s with size = %s",
                      self.volUUID, self.imgUUID, self.sdUUID, newSize)
        # we should return: Success/Failure
        # Backend APIs:
        sizemb = (newSize + 2047) / 2048
        self.vg.extendLV(self.volUUID, sizemb)

    @classmethod
    def changeModifyTime(cls, src_name, dst_name):
        """
        Change last modify time of 'dst' to last modify time of 'src'
        """
        pass

    @classmethod
    def renameVolumeRollback(cls, taskObj, sdUUID, oldUUID, newUUID):
        try:
            cls.log.info("renameVolumeRollback: sdUUID=%s oldUUID=%s newUUID=%s" % (sdUUID, oldUUID, newUUID))
            vg = SDF.produce(sdUUID=sdUUID).vg
            vg.renameLV(oldUUID, newUUID)
        except Exception, e:
            cls.log.info("Failure in renameVolumeRollback: sdUUID=%s oldUUID=%s newUUID=%s" % (sdUUID, oldUUID, newUUID))
            misc.logException(e)

    def rename(self, newUUID, recovery=True):
        """
        Rename volume
        """
        self.log.info("(BlockVolume.rename) Rename volume %s as %s ", self.volUUID, newUUID)
        if not self.imagePath:
            self.validateImagePath()

        if os.path.lexists(self.getVolumePath()):
            os.unlink(self.getVolumePath())

        if recovery:
            name = "Rename volume rollback: " + newUUID
            vars.task.pushRecovery(task.Recovery(name, "blockVolume", "BlockVolume", "renameVolumeRollback",
                                                 [self.sdUUID, newUUID, self.volUUID]))
        self.vg.renameLV(self.volUUID, newUUID)
        self.volUUID = newUUID
        self.volumePath = os.path.join(self.imagePath, newUUID)

    def getDevPath(self):
        """
        Return the underlying device (for sharing)
        """
        return self.vg.lvPath(self.volUUID)

    def setrw(self, rw):
        """
        Set the read/write permission on the volume
        """
        self.vg.setrwLV(self.volUUID, rw)

    def llPrepare(self, rw=False, setrw=False):
        if setrw:
            self.setrw(rw=rw)
        self.vg.activateLV(self.volUUID, rw)

    def llTeardown(self, rw=False, setrw=False):
        if setrw:
            self.setrw(rw=rw)
        self.vg.deactivateLV(self.volUUID)

    def validateImagePath(self):
        """
        Block SD supports lazy image dir creation
        """
        if self.imgUUID == volume.BLANK_UUID:
            imglist = self.findImagesByVolume(self.sdUUID, self.spUUID, self.volUUID, self.vg)
            if len(imglist) >= 1:
                self.imgUUID = imglist[0]
            else:
                raise se.ImagePathError(self.volUUID)
        image_dir = os.path.join(self.storage_repository, self.spUUID,
                                 self.sdUUID, sd.DOMAIN_IMAGES, self.imgUUID)
        if not os.path.isdir(image_dir):
            try:
                os.mkdir(image_dir, 0755)
            except Exception, e:
                misc.logException(e)
                raise se.ImagePathError(image_dir)
        self.imagePath = image_dir

    def validateVolumePath(self):
        """
        Block SD supports lazy volume link creation. Note that the volume can be still inactive.
        An explicit prepare is required to validate that the volume is active.
        """
        if not self.imagePath:
            self.validateImagePath()
        volPath = os.path.join(self.imagePath, self.volUUID)
        if not os.path.lexists(volPath):
            os.symlink(self.vg.lvPath(self.volUUID), volPath)
        self.volumePath = volPath

    @classmethod
    def findImagesByVolume(cls, sdUUID, spUUID, volid, sdvg=None):
        """
        Find the image(s) UUID by one of its volume UUID.
        Templated and shared disks volumes may result more then one image.
        """
        if not sdvg:
            sdvg = SDF.produce(sdUUID=sdUUID).vg
        vollist = sdvg.lvsByTag("PU_%s" % volid)
        vollist.append(volid)
        imglist = []
        for vol in vollist:
            for tag in sdvg.listLVTags(vol):
                if tag.startswith("IU_"):
                    img = tag[3:]
                    if image.REMOVED_IMAGE_PREFIX not in img:
                        imglist.append(img)

        return imglist

    @classmethod
    def getImageVolumes(cls, sdUUID, spUUID, imgUUID):
        """
        Fetch the list of the Volumes UUIDs, not including the shared base (template)
        """
        sdvg = SDF.produce(sdUUID=sdUUID).vg
        return sdvg.lvsByTag("IU_%s" % imgUUID)

    @classmethod
    def getAllChildrenList(cls, sdUUID, spUUID, imgUUID, pvolUUID):
        """
        Fetch the list of children volumes (across the all images in domain)
        """
        sdvg = SDF.produce(sdUUID=sdUUID).vg
        chList = []

        vollist = sdvg.lvsByTag("PU_%s" % pvolUUID)
        for v in vollist:
            for t in sdvg.listLVTags(v):
                if t.startswith("IU_"):
                    chList.append({'imgUUID':t[3:], 'volUUID':v})

        return chList

    def removeMetadata(self, metaid):
        """
        Just wipe meta.
        """
        try:
            self.__putMetadata({ "NONE": "#" * (sd.METASIZE-10) }, metaid)
        except Exception, e:
            misc.propagateError(se.VolumeMetadataWriteError(str(metaid) + str(e)))

    @classmethod
    def __putMetadata(cls, meta, metaid):
        vg = metaid[0]
        offs = metaid[1]
        lines = ["%s=%s\n" % (key.strip(), str(value).strip()) for key,value in meta.iteritems()]
        lines.append("EOF\n")
        misc.writeblockSUDO(vg.lvPath(sd.METADATA), offs * volume.METASIZE, volume.METASIZE, lines)

    @classmethod
    def createMetadata(cls, meta, metaid):
        cls.__putMetadata(meta, metaid)

    def getMetaOffset(self):
        if self.metaoff:
            return self.metaoff

        # FIXME: There is rare case where listing LV tags does not
        #        work for no apperant reason. Trying again usually
        #        works.
        tries = 0
        while tries < 3:
            l = self.vg.listLVTags(self.volUUID)
            for t in l:
                if t.startswith("MD_"):
                    return int(t[3:])

            self.log.warn("MD tag not found please report to support, Volume: %s LV TAGS: %s. Retrying...", self.volUUID, l)
            tries += 1

        self.log.error("(blockVolume.getMetaOffset) missing offset tag on volume %s" %
            self.volUUID)
        raise se.VolumeMetadataReadError("missing offset tag on volume %s" % self.volUUID)

    def getMetadata(self, metaid = None, nocache=False):
        """
        Get Meta data array of key,values lines
        """
        if nocache:
            out = self.metaCache()
            if out:
                return out
        if not metaid:
            vg = self.vg
            offs = self.getMetaOffset()
        else:
            vg = metaid[0]
            offs = metaid[1]
        try:
            meta = misc.readblockSUDO(vg.lvPath(sd.METADATA), offs * volume.METASIZE, volume.METASIZE)
            out = {}
            for l in meta:
                if l.startswith("EOF"):
                    return out
                if l.find("=") < 0:
                    continue
                key,value = l.split("=")
                out[key.strip()] = value.strip()
        except Exception, e:
            misc.propagateError(se.VolumeMetadataReadError(str(metaid) + ":" + str(e)))
        self.putMetaCache(out)
        return out

    def getImage(self):
        img = ''
        for tag in self.vg.listLVTags(self.volUUID):
            if tag.startswith("IU_"):
                img = tag[3:]
        return img

    def setImage(self, imgUUID):
        oldTag = newTag = ""
        for tag in self.vg.listLVTags(self.volUUID):
            if tag.startswith("IU_"):
                oldTag = tag
                break
        if not oldTag:
            raise se.MissingTagOnLogicalVolume(self.volUUID, "IU_")

        newTag = "IU_%s" % imgUUID
        self.vg.changeLVTag(self.volUUID, oldTag, newTag)
        self.setMetaParam(volume.IMAGE, imgUUID)

    def setMetadata(self, metaarr, metaid = None, nocache=False):
        """
        Set the meta data hash as the new meta data of the Volume
        """
        if not metaid:
            metaid = [self.vg, self.getMetaOffset()]
        try:
            self.__putMetadata(metaarr, metaid)
            if not nocache:
                self.putMetaCache(metaarr)
        except Exception, e:
            misc.propagateError(se.VolumeMetadataWriteError(str(metaid) + str(e)))

    def getVolumeSize(self, bs=512):
        """
        Return the volume size in blocks
        """
        # Just call the class method getVSize() - apparently it does what
        # we need. We consider incurred overhead of producing the SD object
        # to be a small price for code de-duplication.
        sdobj = SDF.produce(sdUUID=self.sdUUID)
        return self.getVSize(sdobj, self.imgUUID, self.volUUID, bs)

    getVolumeTrueSize = getVolumeSize

    def getVolumeMtime(self):
        """
        Return the volume mtime in msec epoch
        """
        try:
            mtime = self.getMetaParam(volume.MTIME)
        except se.MetaDataKeyNotFoundError:
            mtime = 0

        return mtime
